import type { Frame, FrameInternal } from './types/Frame'
import { CameraRuntimeError } from './CameraError'
import { CameraModule } from './NativeCameraModule'
import { assertJSIAvailable } from './JSIHelper'
import { WorkletsProxy } from './dependencies/WorkletsProxy'
import type { IWorkletContext } from 'react-native-worklets-core'

declare global {
  // eslint-disable-next-line no-var
  var __frameProcessorRunAtTargetFpsMap: Record<string, number | undefined> | undefined
  // eslint-disable-next-line no-var
  var __ErrorUtils:
    | {
        reportFatalError: (error: unknown) => void
      }
    | undefined
}

type BasicParameterType = string | number | boolean | undefined
type ParameterType = BasicParameterType | BasicParameterType[] | Record<string, BasicParameterType | undefined>

/**
 * An initialized native instance of a FrameProcessorPlugin.
 * All memory allocated by this plugin will be deleted once this value goes out of scope.
 */
export interface FrameProcessorPlugin {
  /**
   * Call the native Frame Processor Plugin with the given Frame and options.
   * @param frame The Frame from the Frame Processor.
   * @param options (optional) Additional options. Options will be converted to a native dictionary
   * @returns (optional) A value returned from the native Frame Processor Plugin (or undefined)
   */
  call(frame: Frame, options?: Record<string, ParameterType>): ParameterType
}

interface TVisionCameraProxy {
  /**
   * @internal
   */
  setFrameProcessor(viewTag: number, frameProcessor: (frame: Frame) => void): void
  /**
   * @internal
   */
  removeFrameProcessor(viewTag: number): void
  /**
   * Creates a new instance of a native Frame Processor Plugin.
   * The Plugin has to be registered on the native side, otherwise this returns `undefined`.
   * @param name The name of the Frame Processor Plugin. This has to be the same name as on the native side.
   * @param options (optional) Options, as a native dictionary, passed to the constructor/init-function of the native plugin.
   * @example
   * ```ts
   * const plugin = VisionCameraProxy.initFrameProcessorPlugin('scanFaces', { model: 'fast' })
   * if (plugin == null) throw new Error("Failed to load scanFaces plugin!")
   * ```
   */
  initFrameProcessorPlugin(name: string, options?: Record<string, ParameterType>): FrameProcessorPlugin | undefined
  /**
   * Throws the given error.
   */
  throwJSError(error: unknown): void
  /**
   * Get the Frame Processor Runtime Worklet Context.
   *
   * This is the serial [DispatchQueue](https://developer.apple.com/documentation/dispatch/dispatchqueue)
   * / [Executor](https://developer.android.com/reference/java/util/concurrent/Executor) the
   * video/frame processor pipeline is running on.
   */
  workletContext: IWorkletContext | undefined
}

const errorMessage = 'Frame Processors are not available, react-native-worklets-core is not installed!'

let hasWorklets = false
let isAsyncContextBusy = { value: false }
let runOnAsyncContext = (_frame: Frame, _func: () => void): void => {
  throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage)
}
let throwJSError = (error: unknown): void => {
  throw error
}

try {
  assertJSIAvailable()

  // this will lazyily require the react-native-worklets-core dependency. If this throws, worklets are not available.
  const Worklets = WorkletsProxy.Worklets

  // fill the stubs with the actual native Worklets implementations
  const throwErrorOnJS = Worklets.createRunOnJS((message: string, stack: string | undefined) => {
    const error = new Error()
    error.message = message
    error.stack = stack
    error.name = 'Frame Processor Error'
    // @ts-expect-error this is react-native specific
    error.jsEngine = 'VisionCamera'

    // From react-native:
    // @ts-expect-error ErrorUtils is in global.
    if (global.ErrorUtils != null)
      // @ts-expect-error the reportFatalError method is an internal method of ErrorUtils not exposed in the type definitions
      global.ErrorUtils.reportFatalError(error)
    else if (global.__ErrorUtils != null) global.__ErrorUtils.reportFatalError(error)
    else console.error('Frame Processor Error:', error)
  })
  throwJSError = (error) => {
    'worklet'
    const safeError = error as Error | undefined
    const message = safeError != null && 'message' in safeError ? safeError.message : 'Frame Processor threw an error.'
    throwErrorOnJS(message, safeError?.stack)
  }

  isAsyncContextBusy = Worklets.createSharedValue(false)
  const asyncContext = Worklets.createContext('VisionCamera.async')
  runOnAsyncContext = asyncContext.createRunAsync((frame: Frame, func: () => void) => {
    'worklet'
    try {
      // Call long-running function
      func()
    } catch (e) {
      // Re-throw error on JS Thread
      throwJSError(e)
    } finally {
      // Potentially delete Frame if we were the last ref
      const internal = frame as FrameInternal
      internal.decrementRefCount()

      isAsyncContextBusy.value = false
    }
  })
  hasWorklets = true
} catch (e) {
  // Worklets are not installed, so Frame Processors are disabled.
}

let proxy: TVisionCameraProxy = {
  initFrameProcessorPlugin: () => {
    throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage)
  },
  removeFrameProcessor: () => {
    throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage)
  },
  setFrameProcessor: () => {
    throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage)
  },
  throwJSError: throwJSError,
  workletContext: undefined,
}
if (hasWorklets) {
  // Install native Frame Processor Runtime Manager
  const result = CameraModule.installFrameProcessorBindings() as unknown
  if (result !== true)
    throw new CameraRuntimeError('system/frame-processors-unavailable', 'Failed to install Frame Processor JSI bindings!')

  // @ts-expect-error global is untyped, it's a C++ host-object
  proxy = global.VisionCameraProxy as TVisionCameraProxy
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
  if (proxy == null) {
    throw new CameraRuntimeError(
      'system/frame-processors-unavailable',
      'Failed to install VisionCameraProxy. Are Frame Processors properly enabled?',
    )
  }
}

export const VisionCameraProxy: TVisionCameraProxy = {
  initFrameProcessorPlugin: proxy.initFrameProcessorPlugin,
  removeFrameProcessor: proxy.removeFrameProcessor,
  setFrameProcessor: proxy.setFrameProcessor,
  throwJSError: throwJSError,
  workletContext: undefined,
}

function getLastFrameProcessorCall(frameProcessorFuncId: string): number {
  'worklet'
  return global.__frameProcessorRunAtTargetFpsMap?.[frameProcessorFuncId] ?? 0
}
function setLastFrameProcessorCall(frameProcessorFuncId: string, value: number): void {
  'worklet'
  if (global.__frameProcessorRunAtTargetFpsMap == null) global.__frameProcessorRunAtTargetFpsMap = {}
  global.__frameProcessorRunAtTargetFpsMap[frameProcessorFuncId] = value
}

/**
 * Runs the given function at the given target FPS rate.
 *
 * For example, if you want to run a heavy face detection algorithm
 * only once per second, you can use `runAtTargetFps(1, ...)` to
 * throttle it to 1 FPS.
 *
 * @param fps The target FPS rate at which the given function should be executed
 * @param func The function to execute.
 * @returns The result of the function if it was executed, or `undefined` otherwise.
 * @worklet
 * @example
 *
 * ```ts
 * const frameProcessor = useFrameProcessor((frame) => {
 *   'worklet'
 *   console.log('New Frame')
 *   runAtTargetFps(5, () => {
 *     'worklet'
 *     const faces = detectFaces(frame)
 *     console.log(`Detected a new face: ${faces[0]}`)
 *   })
 * })
 * ```
 */
export function runAtTargetFps<T>(fps: number, func: () => T): T | undefined {
  'worklet'
  // @ts-expect-error
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  const funcId = func.__workletHash ?? '1'

  const targetIntervalMs = 1000 / fps // <-- 60 FPS => 16,6667ms interval
  const now = performance.now()
  const diffToLastCall = now - getLastFrameProcessorCall(funcId)
  if (diffToLastCall >= targetIntervalMs) {
    setLastFrameProcessorCall(funcId, now)
    // Last Frame Processor call is already so long ago that we want to make a new call
    return func()
  }
  return undefined
}

/**
 * Runs the given function asynchronously, while keeping a strong reference to the Frame.
 *
 * For example, if you want to run a heavy face detection algorithm
 * while still drawing to the screen at 60 FPS, you can use `runAsync(...)`
 * to offload the face detection algorithm to a separate thread.
 *
 * @param frame The current Frame of the Frame Processor.
 * @param func The function to execute.
 * @worklet
 * @example
 *
 * ```ts
 * const frameProcessor = useFrameProcessor((frame) => {
 *   'worklet'
 *   console.log('New Frame')
 *   runAsync(frame, () => {
 *     'worklet'
 *     const faces = detectFaces(frame)
 *     const face = [faces0]
 *     console.log(`Detected a new face: ${face}`)
 *   })
 * })
 * ```
 */
export function runAsync(frame: Frame, func: () => void): void {
  'worklet'

  if (isAsyncContextBusy.value) {
    // async context is currently busy, we cannot schedule new work in time.
    // drop this frame/runAsync call.
    return
  }

  // Increment ref count by one
  const internal = frame as FrameInternal
  internal.incrementRefCount()

  isAsyncContextBusy.value = true

  // Call in separate background context
  runOnAsyncContext(frame, func)
}

/**
 * A private API to wrap a Frame Processor with a ref-counting mechanism
 * @worklet
 * @internal
 */
export function wrapFrameProcessorWithRefCounting(frameProcessor: (frame: Frame) => void): (frame: Frame) => void {
  return (frame) => {
    'worklet'
    // Increment ref-count by one
    const internal = frame as FrameInternal
    internal.incrementRefCount()
    try {
      // Call sync frame processor
      frameProcessor(frame)
    } catch (e) {
      // Re-throw error on JS Thread
      VisionCameraProxy.throwJSError(e)
    } finally {
      // Potentially delete Frame if we were the last ref (no runAsync)
      internal.decrementRefCount()
    }
  }
}
